深入浅出Docker 镜像 | 技术头条
戳蓝字“CSDN云计算”关注我们哦!
技术头条:干货、简洁、多维全面。更多云计算精华知识尽在眼前,get要点、solve难题,统统不在话下!
作为云计算的当红明星Docker 来势汹汹,它就像一场森林大火,烧到了我们中间。因为工作的原因,每天都会和容器云打交道。但是,我发现经常会有些新加入的同事在理解Docker命令方面存在一些问题,尤其是在Docker 镜像底层的工作原理和容器与容器镜像的关系上。
通常情况下一项新的技术诞生时往往会伴随着较多的媒体宣传甚至炒作,且在推广产品的过程中往往会出现一些新的技术名词,让人感觉云里雾里。Docker 的诞生和推广正是在这种情况下进行的。
对于产品本身而言媒体的宣传和炒作可以帮助他们快速的在市场中进行推广,但是这种宣传和炒作也会导致很多用户很难看清被宣传产品的技术的本质,不利于用户对技术的掌握,更加不利于用户对产品的深度使用。
一般情况下只有真正理解了某门技术的原理才能真正掌握这一门技术,接下来才能去深入的使用这门技术。下面我们会由浅入深出的带大家了解下Docker 镜像及其相关技术的原理和本质。
容器 VS 容器镜像
在说Docker 镜像的原理知识之前,我们先看下docker 容器和docker 镜像的区别,因为这部分我们后面会涉及到。
简单说来,我们可以将Docker 镜像看成是Docker 容器的静态时,也可将Docker 容器看成是Docker镜像的运行时。
从Docker 的官方文档来看,Docker 容器的定义和 Docker 镜像的定义几乎是相同,Docker 容器和Docker 镜像的区别主要在于docker 容器多出了一个可写层。
容器中的进程就运行在这个可写层,这个可写层有两个状态,即运行态和退出态。当我们docker run 运行容器后,docker 容器就进入了运行态,当我们停止正在运行中的容器时,docker 容器就进入了退出态。
我们将容器从运行态转为退出态时,期间发生的变更都会写入到容器的文件系统中(需要注意的是,此处不是写入到了docker 镜像中),这方面的变化下文中我们还会再细说。
Docker 存储简介
简单说来Docker 镜像就是一组只读的目录,或者叫只读的 Docker 容器模板,镜像中含有一个Docker 容器运行所需要的文件系统,所以我们说Docker 镜像是启动一个Docker 容器的基础。
如果这样不是很好理解,我们可以通过一个图一起看下:
从图中可以看出除了最上面的一层为读写层之外,下面的其他的层都是只读的镜像层,并且除了最下面的一层外,其他的层都有会有一个指针指向自己下面的一层镜像。
虽然统一文件系统(union file system)技术将不同的镜像层整合成一个统一的文件系统,为构成一个完整容器镜像的层提供了一个统一的视角,隐藏了多个层的复杂性,对用户来说只存在一个文件系统,但图中的这些层并不是不能看到的,如果需要查看的话可以进入运行Docker的机器上进行查看,从这些层中可以看到Docker 内部实现的一些细节,接下来我们一起看下。
一般刚接触Docker 不久的话可能会不太清楚Docker 的存储方式是怎样的,并且可能也不太清楚Docker 容器的镜像到底存储在什么路径下,比较迷茫。
有些同学想了解下Docker 的镜像数据存储在什么位置,然后谷歌了下几篇博文,说是在/var/lib/docker 下有个aufs目录,结果在自己机器上进到这个路径后发现没有aufs相关的目录,然后以为是版本的问题,其实不然。
以Linux服务器为例,其实Docker 的容器镜像和容器本身的数据都存放在服务器的 /var/lib/docker/ 这个路径下。不过不同的linux发行版存储方式上有差别,比如,在ubuntu发行版上存储方式为AUFS,CentOS发行版上的存储方式为device mapper。
/var/lib/docker 路径下的信息在不同的阶段会有变化,从笔者个人经验来看,了解这几个阶段中新增的数据以及容器与镜像的存储结构的变化非常有利于我们对Docker容器以及Docker镜像的理解。
接下来我们一起看下Docker运行后、下载镜像后、运行容器后三个阶段中Docker 存储的变化。
环境信息:
系统发行版:CentOS7.2。
内核版本:3.10.0-327.36.1.el7.x86_64
Docker 版本:1.8
启动Docker后
在此我们假设大家已经安装好了Docker环境,具体安装的过程不再赘述。
# 启动Docker 服务
[root@influxdb ~]# systemctl start docker
# 查看/var/lib/docker路径下的文件结构
[root@localhost docker]# tree .
├── containers
├── devicemapper
│ ├── devicemapper
│ │ ├── data
│ │ └── metadata
│ └── metadata
│ ├── base
│ ├── deviceset-metadata
│ └── transaction-metadata
├── graph
├── linkgraph.db
├── repositories-devicemapper
├── tmp
├── trust
└── volumes
8 directories, 7 files
注:必须启动Docker 服务后查看,如果没有启动Docker 进程则路径/var/lib/docker 不存在。
前文中我们已经提到过,CentOS发行版中Docker 服务使用的存储方式为devicemapper,所以我们从前面tree命令的结果中可以看到出现了目录devicemapper。
有些同学可能会问什么是devicemapper?
太阳底下无新鲜事,devicemapper 并不是伴随着Docker 才出现的,早在linux2.6版本的内核时其实就已经引入了devicemapper,且当时是作为一个很重要的技术出现的。
简单说来devicemapper 就是Docker 服务的一个存储驱动,或者叫Docker 服务的存储后端。Devicemapper 其实是一个基于内核的框架,这个框架中对linux上很多的功能进行了增强,比如对linux上高级卷管理功能的增强。
Devicemapper 存储驱动将我们的每个docker镜像和docker容器都存在在自己的虚拟设备中,devicemapper的这些设备相当于我们常见的一般的写时复制快照设备的超配版本。通过上面的介绍,有些同学可能以为devicemapper 存储驱动是工作在块级别的,但是devicempper 实际是工作在文件级别的,也就是说devicemapper 存储驱动和写时复制操作都是直接操作块,而不是对文件进行操作。
以上是我们关于Docker 服务的devicemapper 存储驱动的一个简单的介绍,Docker 官方文档中提供了很多的有关Docker 存储驱动的介绍,感兴趣的同学可以自行查阅。
进一步的查看的话,可以看到路径/var/lib/docker/devicemapper下面有两个目录,分别为devicemapper和data,其中目录devicemapper 下存在两个名为data和metadata的文件,两个文件从名称即可看出一个是镜像数据的存储池,一个为镜像相关的元数据。接下去我们会逐个看下这个路径下的文件。
进入上面提到的目录metadata下,可以看到这个目录中已经存在三个文件,分别为:base、deviceset-metadata和transction-metadata,这三个文分别用来存放上文中我们提到的元数据的id、大小和uuid等信息。
[root@localhost metadata]# pwd
/var/lib/docker/devicemapper/metadata
[root@localhost metadata]# ls
base deviceset-metadata transaction-metadata
除了上面提到的几个目录,上文中tree的结果中还有几个目录,分别为:containers、devicemapper、graph、linkgraph.db、repositories-devicemapper、tmp、trust和volumes。这几个文件对于docker 的镜像存储来说都很重要,我们以文件repositories-devicemapper为例,看下这个文件对于镜像存储所起到的作用。
[root@localhost docker]# pwd
/var/lib/docker
[root@localhost docker]# ls
containers devicemapper graph linkgraph.db repositories-devicemapper tmp trust volumes
我们可以先看下repositories-devicemapper 这个文件中在当前的阶段中有什么:
root@localhost docker]# cat repositories-devicemapper
{"Repositories":{}}[root@localhost docker]#
从上面cat的结果中我们不难看出,文件repositories-devicemapper 中其实记录的就是docker 镜像的属性信息,比如镜像名称、镜像标签、镜像的ID等信息,如果镜像刚好没有标签的话默认会以lastet作为标签。
以上是对Docker 服务运行后pull镜像之前/var/lib/docker 路径下数据的一个简单的解读,相信大家通过上面的描述已经对docker镜像有了一些更深入的认识。下面我们看下在我们pull 自己的第一个docker镜像之后路径/var/lib/docker 之下的数据会发生怎样的变化。
Pull 镜像后
在此我们以一个nginx镜像为例一起看下这个阶段的变化。
# pull 示例镜像
[root@localhost docker]# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
22f3bf58cd09: Pull complete
ea2fc476f5f0: Pull complete
81d728438afe: Pull complete
303a6dec1190: Pull complete
d43816b44a22: Pull complete
dc02db50a25a: Pull complete
6f650a34b308: Pull complete
a634e96a9de9: Pull complete
72f3ebe1e4d7: Pull complete
c2c9e418b22c: Pull complete
Digest: sha256:a82bbaf63c445ee9b854d182254c62e34e6fa92f63d7b4fdf6cea7e76665e06e
Status: Downloaded newer image for nginx:latest
# 查看镜像是否已经在本地
[root@localhost docker]# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
nginx latest c2c9e418b22c 2 weeks ago 109.3 MB
[root@localhost docker]#
在此我们没有指定nginx镜像的tag,因此默认拉去了最新的版本。然后我们看下路径/var/lib/docker 下是否有变化:
[root@localhost docker]# tree .
.
├── containers
├── devicemapper
│ ├── devicemapper
│ │ ├── data
│ │ └── metadata
……
省略若干数据
……
│ │ ├── checksum
│ │ ├── json
│ │ ├── layersize
│ │ ├── tar-data.json.gz
│ │ └── v1Compatibility
│ ├── 303a6dec11900c97f5d7555d31adec02d2e5e4eaa1a77537e7a5ebd45bb7fcd2
│ │ ├── checksum
│ │ ├── json
│ │ ├── layersize
│ │ ├── tar-data.json.gz
│ │ └── v1Compatibility
│ ├── 6f650a34b3083c96cf8b7babc7a391227c0f78e0d07067071c46e31bd834de3a
│ │ ├── checksum
│ │ ├── json
│ │ ├── layersize
│ │ ├── tar-data.json.gz
│ │ └── v1Compatibility
│ ├── 72f3ebe1e4d793a50836d4e070c94ef7497c80111d178e867014981f64696a39
│ │ ├── checksum
│ │ ├── json
│ │ ├── layersize
│ │ ├── tar-data.json.gz
│ │ └── v1Compatibility
│ ├── 81d728438afe98602e2e692c20299ecf41b93173fb12351c1b59820b17fb16b9
│ │ ├── checksum
│ │ ├── json
│ │ ├── layersize
│ │ ├── tar-data.json.gz
│ │ └── v1Compatibility
│ ├── a634e96a9de9f1e280efaecdd43c7273ac43e109a42ab6c76ab2d2261c8cdc50
│ │ ├── checksum
│ │ ├── json
│ │ ├── layersize
│ │ ├── tar-data.json.gz
│ │ └── v1Compatibility
│ ├──
……
省略若干数据
……
│ ├── ea2fc476f5f055f9e44963987903ecfe0cb480b7e387d8b5cb64006832110afc
│ │ ├── checksum
│ │ ├── json
│ │ ├── layersize
│ │ ├── tar-data.json.gz
│ │ └── v1Compatibility
│ └── _tmp
├── linkgraph.db
├── repositories-devicemapper
├── tmp
├── trust
└── volumes
30 directories, 67 files
[root@localhost docker]#
从结尾的目录数和文件数也可以看出,在我们拉取镜像后/var/lib/docker 下多出了很多的文件(拉取镜像之前,只有8个目录,7个文件),下面我们一步步的分析。
如果这个时候看下路径/var/lib/docker下的文件的话,可以很容易的看到发生变化的主要是下面这三个文件:/var/lib/docker/devicemapper/metadata、/var/lib/docker/devicemapper/mnt以及/var/lib/docker/graph。我们先看下metadata这个文件:
从结果可以看出除了上一个阶段中已经有的base、deviceset-metadata等几个文件外,还多出了一些名字较长的文件,我们挨个看下这几个文件中的数据:
[root@localhost metadata]# cat base
{"device_id":1,"size":107374182400,"transaction_id":1,"initialized":true} [root@localhost metadata]# cat 22f3bf58cd0949b57df2dc161e7026a8cc77699b6a8be7d0e3085e252a5439c3
{"device_id":2,"size":107374182400,"transaction_id":2,"initialized":false}
[root@localhost metadata]# cat ea2fc476f5f055f9e44963987903ecfe0cb480b7e387d8b5cb64006832110afc
{"device_id":3,"size":107374182400,"transaction_id":3,"initialized":false}
[root@localhost metadata]# cat 81d728438afe98602e2e692c20299ecf41b93173fb12351c1b59820b17fb16b9
{"device_id":4,"size":107374182400,"transaction_id":4,"initialized":false} [root@localhost metadata]# cat d43816b44a2280148da8d9b6ce2f357bff9b2e59ef386181f36a4a433a9aad6c
{"device_id":6,"size":107374182400,"transaction_id":6,"initialized":false} [root@localhost metadata]# cat 303a6dec11900c97f5d7555d31adec02d2e5e4eaa1a77537e7a5ebd45bb7fcd2
{"device_id":5,"size":107374182400,"transaction_id":5,"initialized":false} [root@localhost metadata]# cat a634e96a9de9f1e280efaecdd43c7273ac43e109a42ab6c76ab2d2261c8cdc50
{"device_id":9,"size":107374182400,"transaction_id":9,"initialized":false} [root@localhost metadata]# cat dc02db50a25a87ca227492197721e97d19f1822701fe3ec73533a0811a6393a7
{"device_id":7,"size":107374182400,"transaction_id":7,"initialized":false} [root@localhost metadata]# cat 6f650a34b3083c96cf8b7babc7a391227c0f78e0d07067071c46e31bd834de3a
{"device_id":8,"size":107374182400,"transaction_id":8,"initialized":false} [root@localhost metadata]# cat 72f3ebe1e4d793a50836d4e070c94ef7497c80111d178e867014981f64696a39
{"device_id":10,"size":107374182400,"transaction_id":10,"initialized":false} [root@localhost metadata]# cat c2c9e418b22ca5a0b02ef0c2bd02c34ad792d1fc271e5580fdb3252979ccc092
{"device_id":11,"size":107374182400,"transaction_id":11,"initialized":false}
从上面的结果可以看出上面的几个文件中的device_id数值是按照顺序排列下来的,换句话说就是除了上一个阶段中已经存在的base文件,上面结果中其他的几个文件都是nginx镜像的中间镜像层,也就是我们经常执行的命令docker images –a 的结果中看到的构成当前镜像的各个镜像层。
接下来我们再看一个变化较大的文件/var/lib/docker/graph。
[root@localhost graph]# tree .
.
├── 22f3bf58cd0949b57df2dc161e7026a8cc77699b6a8be7d0e3085e252a5439c3
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── 303a6dec11900c97f5d7555d31adec02d2e5e4eaa1a77537e7a5ebd45bb7fcd2
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── 6f650a34b3083c96cf8b7babc7a391227c0f78e0d07067071c46e31bd834de3a
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── 72f3ebe1e4d793a50836d4e070c94ef7497c80111d178e867014981f64696a39
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── 81d728438afe98602e2e692c20299ecf41b93173fb12351c1b59820b17fb16b9
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── a634e96a9de9f1e280efaecdd43c7273ac43e109a42ab6c76ab2d2261c8cdc50
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── c2c9e418b22ca5a0b02ef0c2bd02c34ad792d1fc271e5580fdb3252979ccc092
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── d43816b44a2280148da8d9b6ce2f357bff9b2e59ef386181f36a4a433a9aad6c
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── dc02db50a25a87ca227492197721e97d19f1822701fe3ec73533a0811a6393a7
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
├── ea2fc476f5f055f9e44963987903ecfe0cb480b7e387d8b5cb64006832110afc
│ ├── checksum
│ ├── json
│ ├── layersize
│ ├── tar-data.json.gz
│ └── v1Compatibility
└── _tmp
11 directories, 50 files
从上面的结果中可以很明显的看到我们的镜像nginx及其每一个nginx镜像的中间层对应的目录下都包含如下几个文件:checksum、json、layerrize、tar-data.json.gz和v1Compatibility。我们任意找一个文件看下这几个文件中存放了什么数据。
(1) Checksum
其实从文件名称即可看出每个镜像层中的checksum文件存放的是当前镜像层的md5值,用于核对当前镜像层的数据是否完整。
(2) json
从cat 的结果中可以看出json文件中存放的数据较多,比如Hostname、Domainname、User、Image等信息,而且很多参数和我们经常接触的Dockerfile中的参数相似。相比较前面的checksum文件这个文件中的内容相对较复杂,在此我们也解释下。
此处的json文件中一般主要用于存放镜像中涉及的动态信息,但需要注意的是此处的json文件并不仅仅被用于存储docker镜像的动态信息(很多同学可能会认为此处的json文件只是被用来描述Docker容器的动态信息的),我们在使用Dockerfile 构建镜像时,Dockerfile 构建过程中涉及到的所有操作基本都被记录到这个json文件中。
说到这儿,有些同学可能会问这个json是在什么阶段被使用到的,好问题。通过下面这个图我想大家应该就能看明白了:
从图中我们可以看出每个镜像层的json文件其实是由Docker 守护进程进行解析的。Docker 守护进程通过json文件可以解析出运行容器需要的各种数据,比如环境变量、workdir以及容器启动时需要执行的ENTRYPOINT或者CMD命令等。Docker 守护进程从json文件中获取到这些数据后,接下来就开始进行容器进程的初始化。
(3) layersize
从文件名称即可看出,这个文件中存放的为当前镜像层的占用空间大小:
(4) repositories-devicemapper
上一阶段中我们解释过这个文件中记录的为当前镜像层的属性信息,比如镜像名称信息、镜像标签信息、镜像的ID信息等:
以上是对pull镜像之后运行容器之前镜像存储信息的简单介绍,相信大家在看下之后对docker容器镜像已经有了更加深入的认识。下面我们看下本文中我们要说的最后一个阶段,即运行容器后docker 的存储又发生了哪些变化。
运行容器后
我们运行下前面从dockerhub pull的镜像nginx:latest:
[root@localhost metadata]# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
nginx latest c2c9e418b22c 2 weeks ago 109.3 MB
[root@localhost metadata]#
[root@localhost metadata]#
[root@localhost metadata]#
[root@localhost metadata]# docker run --name nginx -d nginx:latest
814ec80839669e235c94978ed3d07eab0e2b2bebd7d7a64fd6488cddca51be41
[root@localhost metadata]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
814ec8083966 nginx:latest "nginx -g 'daemon off" 3 seconds ago Up 2 seconds 80/tcp nginx
按照惯例,然后我们看下/var/lib/docker路径下的文件结构:
和上一阶段不同,这个阶段发生变化的文件主要是:/var/lib/docker/devicemapper/metadata、/var/lib/docker/devicemapper/mnt以及/var/lib/docker/container,下面我们逐个看下。
(1) metadata
我们看下metadata这个目录下的文件:
从图中的结果可以看出,相比上一个阶段,当前阶段中metadata目录下多出了两个文件,即以51be4e和51b44e-init结尾的两个文件。
我们都知道docker 借助容器镜像运行起容器之后,会在当前镜像的最顶层添加一个特殊的层,和其他的层相比这个层不但有可读的权限还有可写的权限。说到这,相信多出的两个文件的功能就不难理解了。
(2) mnt
在查看mnt下的数据之前,我们先看下这个目录下的文件结构:
对比上面说过的metadata目录,发现这两个目录下的文件是一样的,相比前一个阶段的话也是新增了两个文件,即以51be4e和51b44e-init结尾的两个文件。
(3) container
我们先看下当前目录下的文件结构:
Container目录为容器本身的目录,此目录中存放了诸如容器的配置文件等文件。如果我们删掉这个目录(docker 进程hang死导致docker rm、docker kill杀不掉容器时常用此种方式处理)的话正在运行的容器就会被删掉,我们看下这几个文件都存放了什么数据。
(1) xxx.json.log、config.json
从文件名称即可看出,这两个文件存放的为当前容器的配置信息及其数据:
(2) hosts
hosts配置信息,在此不再赘述。
(3) hostname
容器host名称,可以cat查看后再进入容器查看hostname,核对下看是否是一样的。
(4) resolv.conf
dns配置信息。
小结
前面分析了那么多涉及到docker 存储的文件,在查阅各个文件或者目录作用时可能不是很方便,在此我们给大家总结了一下各个文件的作用(每个文件都是在/var/lib/docker路径下):
(1) devicemapper/devicemapper/data
存储存储池相关的数据。
(2) devicemapper/devicemapper/metdata
存储元数据。
(3) devicemapper/metadata/
存储device_id、layersize等信息。
(4) devicemapper/mnt
存储挂载相关的信息。
(5) container/
存储容器本身的信息。
(6) graph/
存储各个镜像层的详细信息。
(7) repositores-devicemapper
存储镜像的一些基本信息。
(8) tmp
存储docker的临时目录。
(9) trust
存储docker的信任目录。
(10) volumes
存储docker的卷目录。
福利
扫描添加小编微信,备注“姓名+公司职位”,加入【云计算学习交流群】,和志同道合的朋友们共同打卡学习!
推荐阅读:
云计算之基,一文带你速懂虚拟化KVM和XEN
OpenStack网络的下一步原来这么走 | 技术头条
用一枚比特币环游世界? 他是不是疯了...
程序员逆袭为美国最佳 CEO,他说因为爱情
斯坦福区块链匪帮传奇,那些睡地毯、没日没夜写代码的编程少年
Erlang 之父去世,他留给程序员两点忠告
开什么玩笑?股票价格如何经得起AI的推敲?| 技术头条